/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain (a) copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kizitonwose.calendarview.model;

import com.kizitonwose.calendarview.model.enums.DayOwner;
import com.kizitonwose.calendarview.model.enums.InDateStyle;
import com.kizitonwose.calendarview.model.enums.OutDateStyle;
import com.kizitonwose.calendarview.utils.Extensions;
import com.kizitonwose.calendarview.utils.LogUtil;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * MonthConfig
 *
 * @since 2021-02-26
 */
public class MonthConfig {
    private static final String TAG = "MonthConfig";
    private static final int WEEK_NUM = 7;

    /**
     * outDateStyle OutDateStyle
     */
    protected final OutDateStyle outDateStyle;

    /**
     * inDateStyle InDateStyle
     */
    protected final InDateStyle inDateStyle;

    /**
     * maxRowCount int
     */
    protected final int maxRowCount;

    /**
     * startMonth YearMonth
     */
    protected final YearMonth startMonth;

    /**
     * endMonth YearMonth
     */
    protected final YearMonth endMonth;

    /**
     * firstDayOfWeek DayOfWeek
     */
    protected final DayOfWeek firstDayOfWeek;

    /**
     * hasBoundaries boolean
     */
    protected final boolean hasBoundaries;

    private List<CalendarMonth> months;

    /**
     * MonthConfig
     *
     * @param outDateStyle OutDateStyle
     * @param inDateStyle InDateStyle
     * @param maxRowCount int
     * @param startMonth YearMonth
     * @param endMonth YearMonth
     * @param firstDayOfWeek DayOfWeek
     * @param hasBoundaries boolean
     */
    public MonthConfig(OutDateStyle outDateStyle, InDateStyle inDateStyle, int maxRowCount,
                       YearMonth startMonth, YearMonth endMonth, DayOfWeek firstDayOfWeek, boolean hasBoundaries) {
        this.outDateStyle = outDateStyle;
        this.inDateStyle = inDateStyle;
        this.maxRowCount = maxRowCount;
        this.startMonth = startMonth;
        this.endMonth = endMonth;
        this.firstDayOfWeek = firstDayOfWeek;
        this.hasBoundaries = hasBoundaries;
        LogUtil.info(TAG, "hasBoundaries == " + hasBoundaries);
        init();
    }

    private void init() {
        if (hasBoundaries) {
            months = generateBoundedMonths(startMonth, endMonth, firstDayOfWeek,
                maxRowCount, inDateStyle, outDateStyle);
        } else {
            months = generateUnboundedMonths(startMonth, endMonth, firstDayOfWeek,
                maxRowCount, inDateStyle, outDateStyle);
        }
    }

    private List<CalendarMonth> generateBoundedMonths(YearMonth startMonth, YearMonth endMonth,
                                                      DayOfWeek firstDayOfWeek, int maxRowCount,
                                                      InDateStyle inDateStyle, OutDateStyle outDateStyle) {
        int startMonthValue = startMonth.getMonth().getValue();
        int endMonthValue = endMonth.getMonth().getValue();
        LogUtil.info(TAG, "startMonthValue == " + startMonthValue + "  endMonthValue == " + endMonthValue);

        List<CalendarMonth> months = new ArrayList<>();
        YearMonth currentMonth = startMonth;
        while (!currentMonth.isAfter(endMonth)) {
            boolean isGenerateInDates = false;

            if (inDateStyle == InDateStyle.ALL_MONTHS) {
                isGenerateInDates = true;
            } else if (inDateStyle == InDateStyle.FIRST_MONTH) {
                isGenerateInDates = currentMonth.equals(startMonth);
            }

            List<List<CalendarDay>> weekDaysGroup =
                generateWeekDays(currentMonth, firstDayOfWeek, isGenerateInDates, outDateStyle);

            // Group rows by maxRowCount into CalendarMonth classes.
            List<CalendarMonth> calendarMonths = new ArrayList<>();
            int numberOfSameMonth = roundDiv(weekDaysGroup.size(), maxRowCount);

            List<List<List<CalendarDay>>> mList = Extensions.splitList(weekDaysGroup, maxRowCount);
            int num = mList.size();
            for (int i = 0; i < num; i++) {
                calendarMonths.add(new CalendarMonth(currentMonth, mList.get(i), i, numberOfSameMonth));
            }
            months.addAll(calendarMonths);
            if (currentMonth != endMonth) {
                currentMonth = Extensions.getNext(currentMonth);
            } else {
                break;
            }
        }
        return months;
    }

    private List<CalendarMonth> generateUnboundedMonths(
        YearMonth startMonth,
        YearMonth endMonth,
        DayOfWeek firstDayOfWeek,
        int maxRowCount,
        InDateStyle inDateStyle,
        OutDateStyle outDateStyle
    ) {
        // Generate (a) flat list of all days in the given month range
        List<CalendarDay> allDays = new ArrayList<>();
        YearMonth currentMonth = startMonth;

        while (!currentMonth.isAfter(endMonth)) {
            // If inDates are enabled with boundaries disabled,
            // we show them on the first month only.
            boolean isGenerateInDates = false;

            LogUtil.info(TAG, "inDateStyle == " + inDateStyle);
            switch (inDateStyle) {
                case NONE:
                    isGenerateInDates = false;
                    break;
                case ALL_MONTHS:
                    isGenerateInDates = true;
                    break;
                case FIRST_MONTH:
                    isGenerateInDates = currentMonth.equals(startMonth);
                    break;
                default:
                    break;
            }
            List<List<CalendarDay>> mList = generateWeekDays(currentMonth,
                firstDayOfWeek, isGenerateInDates, OutDateStyle.NONE);
            for (List<CalendarDay> list : mList) {
                // We don't generate outDates for any month, they are added manually down below.
                // This is because if outDates are enabled with boundaries disabled, we show them
                // on the last month only.
                allDays.addAll(list);
            }

            if (currentMonth != endMonth) {
                currentMonth = Extensions.getNext(currentMonth);
            } else {
                break;
            }
        }

        // Regroup data into 7 days. Use toList() to create (a) copy of the ephemeral list.
        List<List<CalendarDay>> allDaysGroup = Extensions.splitList(allDays, WEEK_NUM);
        List<CalendarMonth> calendarMonths = new ArrayList<>();
        int calMonthsCount = roundDiv(allDaysGroup.size(), maxRowCount);

        List<List<List<CalendarDay>>> mList = Extensions.splitList(allDaysGroup, maxRowCount);
        int num = mList.size();
        for (int i = 0; i < num; i++) {
            List<List<CalendarDay>> monthWeeks = mList.get(i);
            List<CalendarDay> lastWeek = Extensions.getLast(monthWeeks);

            // Add the outDates for the last row if needed.
            int lastWeekNum = lastWeek.size();
            if (lastWeekNum < WEEK_NUM
                && outDateStyle == OutDateStyle.END_OF_ROW
                || outDateStyle == OutDateStyle.END_OF_GRID) {
                CalendarDay lastDay = lastWeek.get(lastWeekNum - 1);
                List<CalendarDay> outDates = new ArrayList<>();
                int num1 = WEEK_NUM - lastWeekNum;
                for (int j = 1; j <= num1; j++) {
                    outDates.add(new CalendarDay(lastDay.getDate().plusDays(j), DayOwner.NEXT_MONTH));
                }
                lastWeek.addAll(outDates);
                monthWeeks.set(monthWeeks.size() - 1, lastWeek);
            }

            // Add the outDates needed to make the number of rows in this index match the desired maxRowCount.
            int monthWeeksSize = monthWeeks.size();
            while (monthWeeksSize < maxRowCount
                && outDateStyle == OutDateStyle.END_OF_GRID
                // This will be true when we add the first inDates and the last week row in the CalendarMonth is not filled up.
                || monthWeeksSize == maxRowCount
                && monthWeeks.get(monthWeeksSize - 1).size() < WEEK_NUM
                && outDateStyle == OutDateStyle.END_OF_GRID
            ) {
                CalendarDay lastDay = Extensions.getLast(Extensions.getLast(monthWeeks));
                List<CalendarDay> nextRowDates = new ArrayList<>();
                for (int j = 1; j <= WEEK_NUM; j++) {
                    nextRowDates.add(new CalendarDay(lastDay.getDate().plusDays(j), DayOwner.NEXT_MONTH));
                }
                if (monthWeeks.get(monthWeeksSize - 1).size() < WEEK_NUM) {
                    List<CalendarDay> mList1 = monthWeeks.get(monthWeeksSize - 1);
                    mList1.addAll(nextRowDates);

                    monthWeeks.set(monthWeeksSize - 1, Extensions.take(mList1, WEEK_NUM));
                } else {
                    monthWeeks.add(nextRowDates);
                }
            }
            calendarMonths.add(
                // numberOfSameMonth is the total number of all months and
                // indexInSameMonth is basically this item's index in the entire month list.
                new CalendarMonth(startMonth, monthWeeks, calendarMonths.size(), calMonthsCount)
            );
        }

        return calendarMonths;
    }

    private List<List<CalendarDay>> generateWeekDays(YearMonth yearMonth, DayOfWeek firstDayOfWeek,
                                                     boolean isGenerateInDates, OutDateStyle outDateStyle) {
        int year = yearMonth.getYear();
        int month = yearMonth.getMonthValue();

        List<CalendarDay> thisMonthDays = new ArrayList<>();

        for (int i = 1; i <= yearMonth.lengthOfMonth(); i++) {
            thisMonthDays.add(new CalendarDay(LocalDate.of(year, month, i), DayOwner.THIS_MONTH));
        }

        List<List<CalendarDay>> weekDaysGroup;

        if (isGenerateInDates) {
            TemporalField weekOfMonthField = WeekFields.of(firstDayOfWeek, 1).weekOfMonth();
            List<List<CalendarDay>> groupByWeekOfMonth = new ArrayList<>();
            HashMap<Integer, List<CalendarDay>> map = new HashMap<>();
            for (CalendarDay calendarDay : thisMonthDays) {
                int key = calendarDay.getDate().get(weekOfMonthField);
                if (!map.containsKey(key)) {
                    List<CalendarDay> mList = new ArrayList<>();
                    mList.add(calendarDay);
                    map.put(key, mList);
                } else {
                    map.get(key).add(calendarDay);
                }
            }
            groupByWeekOfMonth.addAll(map.values());

            List<CalendarDay> firstWeek = groupByWeekOfMonth.get(0);
            if (firstWeek.size() < WEEK_NUM) {
                YearMonth previousMonth = yearMonth.minusMonths(1);
                List<CalendarDay> inDates = new ArrayList<>();
                int size = previousMonth.lengthOfMonth();
                int start = size - (WEEK_NUM - firstWeek.size()) + 1;
                for (int i = start; i <= size; i++) {
                    inDates.add(new CalendarDay(LocalDate.of(previousMonth.getYear(), previousMonth.getMonth(), i),
                        DayOwner.PREVIOUS_MONTH));
                }
                inDates.addAll(firstWeek);
                groupByWeekOfMonth.set(0, inDates);
            }
            weekDaysGroup = groupByWeekOfMonth;
        } else {
            // Group days by 7, first day shown on the month will be day 1.
            // Use toMutableList() to create a copy of the ephemeral list.
            weekDaysGroup = Extensions.splitList(thisMonthDays, WEEK_NUM);
        }

        if (outDateStyle == OutDateStyle.END_OF_ROW || outDateStyle == OutDateStyle.END_OF_GRID) {
            // Add out-dates for the last row.
            List<CalendarDay> lastWeek = weekDaysGroup.get(weekDaysGroup.size() - 1);
            int size = lastWeek.size();
            if (size < WEEK_NUM) {
                CalendarDay lastDay = lastWeek.get(size - 1);
                List<CalendarDay> outDates = new ArrayList<>();
                int addSize = WEEK_NUM - size;
                for (int i = 1; i <= addSize; i++) {
                    outDates.add(new CalendarDay(lastDay.getDate().plusDays(i), DayOwner.NEXT_MONTH));
                }
                lastWeek.addAll(outDates);
                weekDaysGroup.set(weekDaysGroup.size() - 1, lastWeek);
            }

            // Add more rows to form a 6 x 7 grid
            if (outDateStyle == OutDateStyle.END_OF_GRID) {
                while (weekDaysGroup.size() < 6) {
                    CalendarDay lastDay = CalendarMonth.getLast(weekDaysGroup);
                    List<CalendarDay> nextRowDates = new ArrayList<>();

                    for (int i = 1; i <= WEEK_NUM; i++) {
                        nextRowDates.add(new CalendarDay(lastDay.getDate().plusDays(i), DayOwner.NEXT_MONTH));
                    }

                    weekDaysGroup.add(nextRowDates);
                }
            }
        }

        for (int i = 0; i < weekDaysGroup.size(); i++) {
            List<CalendarDay> calendarDays = weekDaysGroup.get(i);
            for (int j = 0; j < calendarDays.size(); j++) {
                CalendarDay calendarDay = calendarDays.get(j);
                int day = calendarDay.getDay();
                LogUtil.info(TAG, "day == " + day);
            }
        }
        return weekDaysGroup;
    }

    public OutDateStyle getOutDateStyle() {
        return outDateStyle;
    }

    public InDateStyle getInDateStyle() {
        return inDateStyle;
    }

    public int getMaxRowCount() {
        return maxRowCount;
    }

    public YearMonth getStartMonth() {
        return startMonth;
    }

    public YearMonth getEndMonth() {
        return endMonth;
    }

    public DayOfWeek getFirstDayOfWeek() {
        return firstDayOfWeek;
    }

    public boolean isHasBoundaries() {
        return hasBoundaries;
    }

    private int roundDiv(int first, int other) {
        int div = first / other;
        int rem = first % other;
        return rem == 0 ? div : div + 1;
    }

    public List<CalendarMonth> getMonths() {
        return months;
    }
}
